/*
 * Licensed to Nanjing RedOrange Co. ltd (RedOrange) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * RedOrange licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package redora.rdo.generator;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

import org.apache.commons.io.IOUtils;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import redora.rdo.generator.Template.Destination;
import redora.rdo.generator.Template.Type;
import redora.rdo.generator.exceptions.ModelGenerationException;
import freemarker.cache.ClassTemplateLoader;
import freemarker.cache.FileTemplateLoader;
import freemarker.cache.MultiTemplateLoader;
import freemarker.cache.TemplateLoader;
import freemarker.core.ParseException;
import freemarker.ext.dom.NodeModel;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

/**
 * Wrapper for Freemarker template engine. It generates files on a given
 * Template. Part of this class are the several directories where for example
 * the generated source files are located.
 * 
 * @see Templates
 *@author jaap
 */
public class ModelFileHandler {
	/** project build directory, default target */
	private final String buildDir;
	/** project source directory, default src/main/java */
	private final String sourceDir;
	/** project source directory, default src/main/webapp */
	private final String webappDir;
	/** project resource directory, default src/main/resources */
	private final String resourceDir;
	private Configuration freemarkerConf;
	
	private TemplateHandler tHandler;

	/**
	 * Initializes and set the Freemarker (caching) configuration. This also means that Freemarker
	 * templates can be located in the usual location (the root of the classpath), a custom location
	 * (in /templates of the classpath) and locally in the p-templates/resources for use in this project.
	 * 
	 * @param sourceDir
	 * @param buildDir
	 * @param resourceDir
	 * @throws ModelGenerationException
	 */
	public ModelFileHandler(String sourceDir, String buildDir,
			String resourceDir, String webappDir) throws ModelGenerationException {
		this.sourceDir = sourceDir;
		this.buildDir = buildDir;
		this.resourceDir = resourceDir;
		this.webappDir = webappDir;
		try {
			this.tHandler = new TemplateHandler();
		} catch (Exception e) {
			throw new ModelGenerationException("Failed to create TemplateHandler", e);
		}
		if (findModelFiles().isEmpty())
			throw new ModelGenerationException("Cannot find any model in "
					+ resourceDir);

		// Prepare Freemarker
		freemarkerConf = new Configuration();
		ClassTemplateLoader custom = new ClassTemplateLoader(getClass(), "/templates");
		ClassTemplateLoader standard = new ClassTemplateLoader(getClass(), "/");
		File tplDir = new File(".." + File.separator + "p-templates" + File.separator + resourcesDir());
		if (!tplDir.exists()) {
			tplDir = new File(localTemplatesDir());
		}
		try {
			FileTemplateLoader local = new FileTemplateLoader(tplDir);
			freemarkerConf.setTemplateLoader(new MultiTemplateLoader(new TemplateLoader[] { local, custom, standard }));			
		} catch (IOException e) {
			freemarkerConf.setTemplateLoader(new MultiTemplateLoader(new TemplateLoader[] { custom, standard }));
		}
		freemarkerConf.setTemplateUpdateDelay(9000);
		freemarkerConf.addAutoInclude("includeMacro.ftl");
	}

	/**
	 * src/main/resources/
	 */
	public final String resourcesDir() {
		return resourceDir + File.separator;
	}

	/**
	 * src/main/resources/model/
	 */
	public final String modelDir() {
		return resourcesDir() + "model" + File.separator;
	}

	/**
	 * src/main/resources/templates/
	 */
	public final String localTemplatesDir() {
		return resourcesDir() + "templates" + File.separator;
	}
	
	/**
	 * src/main/resources/model/inlcude/
	 */
	public final String includeDir() {
		return modelDir() + "includes" + File.separator;
	}

	/**
	 * target/generated-sources/
	 */
	public final String generatedSourcesDir() {
		return buildDir + File.separator + "generated-sources" + File.separator;
	}

	
	/**
	 * Creates the generated source file based on given template and model
	 * document.
	 */
	public void process(String templateFile, Document model, Destination destination, String packageName, String outFileName, Type type, HashMap<String, String> param)
			throws ModelGenerationException, FileNotFoundException, SAXException, ParserConfigurationException, IOException {
		System.out.println("Processing with " + templateFile + " to " + destination + " - " + packageName + "/" + outFileName);

		String destinationPath = generatedSourcesDir();
		if (destination == Destination.source) {
			if (hasSourceFile(outFileName, packageName)) {
				System.out.println("Stub " + outFileName + " already exists.");
				return;
			}
			destinationPath = sourceDir + File.separator;
		} else if (destination == Destination.webapp) {
			if (hasWebappFile(outFileName, packageName)) {
				System.out.println("Stub " + outFileName + " already exists.");
				return;
			}
			destinationPath = webappDir + File.separator;			
		}
		
		// -------> Prepare Output
		createPath(destinationPath + packageToPath(packageName));
		Writer out;
		try {
			out = new OutputStreamWriter(new FileOutputStream(destinationPath
					+ packageToPath(packageName)
					+ outFileName));
		} catch (FileNotFoundException e) {
			throw new ModelGenerationException("Can't find: " + destinationPath
					+ packageToPath(packageName)
					+ outFileName, e);
		}

		if (type == Type.freemarker) {
			// Input ----->
			Map<String, NodeModel> root = new HashMap<String, NodeModel>();
			root.put("doc", NodeModel.wrap(model));

			try {
				Template template = freemarkerConf.getTemplate(templateFile);
				template.process(root, out);
			} catch (ParseException e) {
				throw new ModelGenerationException(
						"There is a error in template: " + templateFile
								+ ". I found it when generating " + outFileName,
						e);
			} catch (IOException e) {
				throw new ModelGenerationException("Can't find " + templateFile
						+ ".ftl when generating " + outFileName, e);
			} catch (TemplateException e) {
				throw new ModelGenerationException(
						"There is a error in template: " + templateFile
								+ ". I found it when generating " + outFileName,
						e);
			} catch (RuntimeException e) {
				throw new ModelGenerationException(
						"There is another error while trying this template: "
								+ templateFile + ". I found it when generating "
								+ outFileName, e);
			}
		} else if (type == Type.xslt) {
			try {
				XMLUtil.xsltTransform(model.getFirstChild(), tHandler.getFile(templateFile), out,
						param);
			} catch (TransformerException e) {
				throw new ModelGenerationException(
						"Sorry, i failed to use this template: " + templateFile
								+ ". It broke when generating " + outFileName,
						e);
			}
		} else if (type == Type.copy) {
			IOUtils.copy(tHandler.getFile(templateFile), out);
		}
	}
	
	
	/**
	 * Create path for generated-sources if the path is not existed.
	 * */
	public static void createPath(String path) {
		File dir = new File(path);
		if (!dir.exists()) {
			dir.mkdirs();
			System.out.println("Created path: " + dir.getPath());
		}
	}

	/**
	 * Get all model documents file list
	 * 
	 * @return All "???.xml" files in the model directory (no subdiretories)
	 */
	public Set<String> findModelFiles() {
		Set<String> retVal = new HashSet<String>();
		File dir = new File(modelDir());
		String[] model = dir.list(new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return name.endsWith(".xml");
			}
		});

		if (model != null && model.length > 0)
			Collections.addAll(retVal, model);

		return retVal;
	}

	/**
	 * Get all include documents file list
	 * 
	 * @return All "???.xml" files in the include directory (no subdiretories)
	 */
	public Set<String> findIncludeFiles() {
		Set<String> retVal = new HashSet<String>();
		File dir = new File(includeDir());
		String[] model = dir.list(new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return name.endsWith(".xml");
			}
		});

		if (model != null && model.length > 0)
			Collections.addAll(retVal, model);
		return retVal;
	}

	/** Check if the source file exists. */
	public boolean hasSourceFile(String fileName, String packageName) {
		return new File(sourceDir + File.separator + packageToPath(packageName)
				+ fileName).exists();
	}
	/** Check if the webapp file exists. */
	public boolean hasWebappFile(String fileName, String packageName) {
		return new File(webappDir + File.separator + packageToPath(packageName)
				+ fileName).exists();
	}

	/**
	 * Convert the package to the real file path
	 * */
	public static String packageToPath(String packageName) {
		StringTokenizer pathToken = new StringTokenizer(packageName, ".");
		String retVal = "";
		while (pathToken.hasMoreElements()) {
			retVal += pathToken.nextToken();
			retVal += File.separator;
		}

		return retVal;
	}
}
